/*******************************************************************************
 * Copyright (c) 2004, 2007 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Sebastian Davids - fix for the bug 178028 (removals not propagated to handlers)
 *******************************************************************************/
package org.aseclipse.core.runtime.registry.classes {

import flash.utils.Dictionary;

import mx.collections.ArrayCollection;
import mx.core.Application;

import org.aseclipse.core.runtime.*;
import org.aseclipse.core.runtime.registry.IExtension;
import org.aseclipse.core.runtime.registry.IExtensionChangeHandler;
import org.aseclipse.core.runtime.registry.IExtensionDelta;
import org.aseclipse.core.runtime.registry.IExtensionPoint;
import org.aseclipse.core.runtime.registry.IExtensionRegistry;
import org.aseclipse.core.runtime.registry.RegistryChangeEvent;

/**
 * Implementation of the IExtensionTracker.
 * <p>
 * This class can be used without OSGi running.
 * </p>
 * @see org.eclipse.core.runtime.dynamichelpers.IExtensionTracker 
 * @since 3.1
 */
public class ExtensionTracker implements IExtensionTracker {
	//Map keeping the association between extensions and a set of objects. Key: IExtension, value: ReferenceHashSet.
	private var extensionToObjects:Dictionary = new Dictionary();
	private var handlers:Array = new Array();
	private var lock:Boolean = false;
	private var closed:Boolean = false;
	private var registry:ExtensionRegistry; // the registry that this tacker works with

	/**
	 * Construct a new instance of the extension tracker.
	 */
	/* public ExtensionTracker() {
		this(RegistryFactory.getRegistry());
	} */

	/**
	 * Construct a new instance of the extension tracker using the given registry
	 * containing tracked extensions and extension points.
	 * 
	 * @param theRegistry the extension registry to track 
	 * @since org.eclipse.equinox.registry 3.2
	 */
	public function ExtensionTracker(theRegistry:IExtensionRegistry) {
		registry = theRegistry as ExtensionRegistry;
		if (registry != null) {
			registry.addEventListener(RegistryChangeEvent.REGISTRY_CHANGE, registryChanged);
		} else {
			// RuntimeLog.log(new Status(IStatus.ERROR, RegistryMessages.OWNER_NAME, 0, RegistryMessages.registry_no_default, null));
			throw new Error("Error: registry is not initialized when tracker startup");
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.runtime.dynamichelpers.IExtensionTracker#registerHandler(org.eclipse.core.runtime.dynamichelpers.IExtensionChangeHandler, org.eclipse.core.runtime.dynamichelpers.IFilter)
	 */
	public function registerHandler(handler:IExtensionChangeHandler, filter:Function):void {
		if(lock == false) {
			if (closed)
				return;
			// TODO need to store the filter with the handler
			handlers.push({handler:handler, filter:filter});
		} else {
		    Application.application.callLater(registerHandler, [handler, filter]);
		}
	}

	/* (non-Javadoc)
	 * @see IExtensionTracker@unregisterHandler(IExtensionChangeHandler)
	 */
	public function unregisterHandler(handler:IExtensionChangeHandler):void {
		if(lock == false) {
			if (closed)
				return;
			lock = true;	
			var list:ArrayCollection = new ArrayCollection(handlers);
			for(var i:int=0;i<list.length;i++) {
			    var item:Object = list.getItemAt(i);
			    if(item.handler == handler) {
			        list.removeItemAt(i);
			        break;
			    }
			}
			lock = false;
		} else {
		    Application.application.callLater(unregisterHandler, [handler]);
		}
	}

	/* (non-Javadoc)
	 * @see IExtensionTracker@registerObject(IExtension, Object, int)
	 */
	public function registerObject(extension:IExtension, object:Object, useWeakReference:Boolean = false):void {
	// public void registerObject(IExtension element, Object object, int referenceType) {
		if (extension == null || object == null)
			return;

		if(lock == false) {
			if (closed)
				return;
            lock = true;
			var associatedObjects:Dictionary = extensionToObjects[extension] as Dictionary;
			if (associatedObjects == null) {
				associatedObjects = new Dictionary();
				extensionToObjects[extension] = associatedObjects;
			}
			associatedObjects[object] = useWeakReference;
			lock = false;
		} else {
		    Application.application.callLater(registerObject, [extension, object, useWeakReference]);
		}
	}

	/**
	 * Implementation of IRegistryChangeListener interface.
	 * <p>
	 * <em>This method must not be called by clients.</em>
	 * </p>
	 */
	public function registryChanged(event:RegistryChangeEvent):void {
		var delta:Array = event.getExtensionDeltas(null);
		var len:int = delta.length;
		for (var i:int = 0; i < len; i++)
			switch ((delta[i] as IExtensionDelta).kind) {
				case ExtensionDelta.ADDED :
					doAdd(delta[i]);
					break;
				case ExtensionDelta.REMOVED :
					doRemove(delta[i]);
					break;
				default :
					break;
			}
	}

	/**
	 * Notify all handlers whose filter matches that the given delta occurred.
	 * If the list of objects is not <code>null</code> then this is a removal and 
	 * the handlers will be given a chance to process the list.  If it is <code>null</code>
	 * then the notification is an addition.
	 * 
	 * @param delta the change to broadcast
	 * @param objects the objects to pass to the handlers on removals
	 */
	private function notify(delta:IExtensionDelta, objects:Array):void {
		// Get a copy of the handlers for safe notification
		var handlersCopy:Array = new Array();
		if(lock == false) {
			if (closed)
				return;
			if (handlers == null || handlers.isEmpty())
				return;
			for each(var obj:Object in handlers) {
			    handlersCopy.push(obj);
			}
		} else {
		    Application.application.callLater(notify, [delta, objects]);
		    return;
		}

		for (var i:int = 0; i < handlersCopy.length; i++) {
			var wrapper:Object = handlersCopy[i];
			if (wrapper.filter == null || (wrapper.filter as Function).call(this, delta.extensionPoint) == true) {
				if (objects == null)
					applyAdd(wrapper.handler, delta.extension);
				else
					applyRemove(wrapper.handler, delta.extension, objects);
			}
		}
	}

	protected function applyAdd(handler:IExtensionChangeHandler, extension:IExtension):void {
		handler.addExtension(this, extension);
	}

	private function doAdd(delta:IExtensionDelta):void {
		notify(delta, null);
	}

	private function doRemove(delta:IExtensionDelta):void {
		var removedObjects:Array = null;
		if (lock == false) {
			if (closed)
				return;
            lock = true;
			var associatedObjects:Dictionary = extensionToObjects[delta.extension] as Dictionary;
			delete extensionToObjects[delta.extension];
			if (associatedObjects == null)
				removedObjects = new Array();
			else
				//Copy the objects early so we don't hold the lock too long
				removedObjects = toArray(associatedObjects);
			lock = false;	
		} else {
		    Application.application.callLater(doRemove, [delta]);
		    return;
		}
		notify(delta, removedObjects);
	}

    private function toArray(dic:Dictionary):Array {
        var array:Array = new Array();
        for (var key:Object in dic){
            array.push(key);
        }
        return array;
    }
    
	protected function applyRemove(handler:IExtensionChangeHandler, removedExtension:IExtension, removedObjects:Array):void {
		handler.removeExtension(removedExtension, removedObjects);
	}

	/* (non-Javadoc)
	 * @see IExtensionTracker@getObjects(IExtension)
	 */
	 // not safe
	public function getObjects(element:IExtension):Array {
		if (closed)
			return new Array();
		var objectSet:Dictionary = extensionToObjects[element] as Dictionary;
		if (objectSet == null)
			return new Array();
		return toArray(objectSet);
	}

	/* (non-Javadoc)
	 * @see IExtensionTracker@close()
	 */
	public function close():void {
		if (lock == false) {
			if (closed)
				return;
			lock = true;
			if (registry != null) {
				// registry.removeRegistryChangeListener(this);
				registry.removeEventListener(RegistryChangeEvent.REGISTRY_CHANGE, registryChanged);
			}
			extensionToObjects = null;
			handlers = null;
			closed = true;
			lock = false;
		} else {
		    Application.application.callLater(close, null);
		}
	}

	/* (non-Javadoc)
	 * @see IExtensionTracker@unregisterObject(IExtension, Object)
	 */
	public function unregisterObject(extension:IExtension, object:Object):void {
		if (lock == false) {
			if (closed)
				return;
			lock = true;
			var associatedObjects:Dictionary = extensionToObjects[extension] as Dictionary;
			if (associatedObjects != null)
				delete associatedObjects[object];
			lock = false;
		} else {
		    Application.application.callLater(unregisterObject, [extension, object]);
		}
	}

	/* (non-Javadoc)
	 * @see IExtensionTracker@unregisterObject(IExtension)
	 */
	public function unregisterObjects(extension:IExtension):void {
		if (lock == false) {
			if (closed)
				return;
			lock = true;
			delete extensionToObjects[extension];
			lock = false;
		} else {
		    Application.application.callLater(unregisterObjects, [extension]);
		}
	}

	/**
	 * Return an instance of filter matching all changes for the given extension point.
	 * 
	 * @param xpt the extension point 
	 * @return a filter
	 */
	public static function createExtensionPointFilter(xpt:IExtensionPoint):Function {
		return function(target:IExtensionPoint):Boolean {
				return xpt == target;
			};
	}

	/**
	 * Return an instance of filter matching all changes for the given extension points.
	 * 
	 * @param xpts the extension points array used to filter
	 * @return a filter
	 */
	public static function createExtensionPointsFilter(xpts:Array):Function {
		return function(target:IExtensionPoint):Boolean {
				for each(var item:IExtensionPoint in xpts)
					if (item == target)
						return true;
				return false;
			};
	}

}
}